fix(logging): close three ways the JSON fallback still lost the record - #1515
Conversation
#1491 wrapped the `json.dumps` call so a bad enrichment could not cost the whole record, and stated the guarantee as "never lose a record to a serialization error". The wrapper holds for the two inputs it was written against, but the *recovery* path it added can itself raise — so the guarantee covered the anticipated failures rather than the property. Measured against a real handler on 8517bf8, healthy -> poisoned -> healthy: | input | before | after | |------------------------------------|--------|-------| | circular container | 3/3 | 3/3 | | exploding `__str__` | 3/3 | 3/3 | | exception whose own `__str__` raises | 2/3 | 3/3 | | value forging `__class__ = str` | 2/3 | 3/3 | | int past the 4300-digit cap | 2/3 | 3/3 | | non-finite float | 3/3* | 3/3 | * emitted, but as a bare `NaN`/`Infinity` literal, which is not valid JSON — a strict downstream parser rejects the record, which is the same loss moved to the consumer. Each cause, and the fix: * The fallback built `f"{type(exc).__name__}: {exc}"` directly. The exception it catches may be one raised from a call site's own `__str__`, so describing the failure became the failure. Now `_describe_exception`, which falls back to the type name. * The scalar filter used `isinstance`, which consults `__class__` and can be forged with a property returning `str`. `json` dispatches on the real runtime type, so such a value passed the filter and then raised in the fallback's own dump. Now matched on exact runtime type. * `int` is a scalar by every type test, but `json` renders ints via `str` and CPython caps that at `sys.get_int_max_str_digits()` (4300). Now bounded by `bit_length`, which avoids performing the conversion being guarded against. * `allow_nan=False`, so a non-finite float routes to the fallback and the record stays valid JSON instead of carrying a JavaScript literal. A final constant-record tier keeps the guarantee a property of the code rather than of the failure modes anticipated here — the exact gap #1452 was about. It is not reachable through any input above, and the comment says so. Tests: +10 in the existing CWE-117 file. All 10 fail against 8517bf8 and pass after (7 failed / 26 passed -> 33 passed); the pre-existing 26 are unchanged, so this does not weaken what #1491 established. Full `tests/unit` is unchanged at 1637 failed / 76 errors (missing optional deps in this environment) with +10 passed, i.e. no regressions. ruff clean; mypy unchanged at its 17 pre-existing errors in this module. Refs #1452 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHLdfqAJcfL9LPWC7Dp9Gx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
One remaining hole:
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
|
Re-triggering governance after fixing Closes #1525 and full template sections. |
#1515 closed #1525's three residual holes and is on main. One path in the same function is still reachable, and it is the one its docstring asserts is safe: "The type name is a plain attribute lookup and is always safe." It is not. `__name__` on a class is looked up on its *metaclass*, so a metaclass defining `__name__` as a raising property defeats the `except` branch. That second raise happens outside any guard, so it propagates past the `_JSON_UNSERIALIZABLE_RECORD` tier and out of `_format_json` entirely, and `Handler.handleError` drops the record. The constant-record tier does not catch it. Measured on 5473bcc: 2 of 3 records reach the sink. `object.__getattribute__(type(exc), "__name__")` does not fix this -- it still routes through the metaclass descriptor: type(exc).__name__ -> RAISES object.__getattribute__(type(exc),"__name__") -> RAISES type.__dict__["__name__"].__get__(type(exc)) -> OK Binding the descriptor from `type.__dict__` bypasses an override and returns the ordinary name for ordinary classes, with a constant as the final floor. The docstring is corrected to state the guarantee the code provides. This is the same failure shape #1525 was filed about, one level down: the recovery step for a failure is itself able to fail. Verification on this head: 36 passed in tests/unit/test_logging_config_crlf.py (33 pre-existing, unchanged). Reverting only logging_config.py: 2 failed, 34 passed. The third new test, test_describe_exception_is_unchanged_for_ordinary_exceptions, passes on 5473bcc by design -- it guards the normal path against regression rather than pinning the fix, so it is excluded from the non-vacuity claim. ruff clean; mypy reports the same 17 pre-existing errors on both heads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Msg6kqkhiuW1ZiDv66sr4N
fix(logging): stop a hostile metaclass costing the record (#1576) #1515 closed #1525's three residual holes and is on main. One path in the same function is still reachable, and it is the one its docstring asserts is safe: "The type name is a plain attribute lookup and is always safe." It is not. `__name__` on a class is looked up on its *metaclass*, so a metaclass defining `__name__` as a raising property defeats the `except` branch. That second raise happens outside any guard, so it propagates past the `_JSON_UNSERIALIZABLE_RECORD` tier and out of `_format_json` entirely, and `Handler.handleError` drops the record. The constant-record tier does not catch it. Measured on 5473bcc: 2 of 3 records reach the sink. `object.__getattribute__(type(exc), "__name__")` does not fix this -- it still routes through the metaclass descriptor: type(exc).__name__ -> RAISES object.__getattribute__(type(exc),"__name__") -> RAISES type.__dict__["__name__"].__get__(type(exc)) -> OK Binding the descriptor from `type.__dict__` bypasses an override and returns the ordinary name for ordinary classes, with a constant as the final floor. The docstring is corrected to state the guarantee the code provides. This is the same failure shape #1525 was filed about, one level down: the recovery step for a failure is itself able to fail. Verification on this head: 36 passed in tests/unit/test_logging_config_crlf.py (33 pre-existing, unchanged). Reverting only logging_config.py: 2 failed, 34 passed. The third new test, test_describe_exception_is_unchanged_for_ordinary_exceptions, passes on 5473bcc by design -- it guards the normal path against regression rather than pinning the fix, so it is excluded from the non-vacuity claim. ruff clean; mypy reports the same 17 pre-existing errors on both heads. Claude-Session: https://claude.ai/code/session_01Msg6kqkhiuW1ZiDv66sr4N Co-authored-by: Claude <noreply@anthropic.com>
Canonical issue
Closes #1525
Outcome
A JSON log record is no longer lost when the recovery path after a serialization error itself raises, when a value forges
__class__ = str, when an int exceeds the digit-string cap, or when a non-finite float would emit invalid JSON. The guarantee is a property of the code (final constant-record tier), not of the failure modes anticipated in #1491.Scope
_format_jsonrecovery (describe exception, exact-type scalar filter, int bit-length bound,allow_nan=False), +10 unit tests in the existing CWE-117 suiteRisk
git revert— no schema or config changeVerification
test,bandit,trivy, security scans green on PR headProduction evidence
Logging-only change. Unit tests carry the evidence; Vercel preview is not a meaningful runtime signal for Python logging.
Agent handoff
Land after Canonical issue + PR Governance pass. Follow-up to #1491 / #1452 residual holes.